/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.api.project.server.type;

import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.model.project.type.Attribute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Registry for Project Type definitions on server
 * <p>All the Project Types definitions should be registered here
 *
 * @author gazarenkov
 */
@Singleton
public class ProjectTypeRegistry {

    private static final Logger LOG = LoggerFactory.getLogger(ProjectTypeRegistry.class);

    public static final  ProjectTypeDef          BASE_TYPE                  = new BaseProjectType();
    public static final  ChildToParentComparator CHILD_TO_PARENT_COMPARATOR = new ChildToParentComparator();
    private static final Pattern                 NAME_PATTERN               = Pattern.compile("[^a-zA-Z0-9-_.]");

    private final Map<String, ProjectTypeDef> projectTypes;
    private final Map<String, ProjectTypeDef> validatedData;

    /**
     * Initialises Set of Project Type definitions
     *
     * @param types
     */
    @Inject
    public ProjectTypeRegistry(Set<ProjectTypeDef> types) {
        projectTypes = new HashMap<>();
        validatedData = new HashMap<>();

        validate(types);

        for (ProjectTypeDef type : validatedData.values()) {
            try {
                init(type);
            } catch (ProjectTypeConstraintException e) {
                LOG.error(e.getMessage());
            }
        }
    }

    /**
     * Registers single projectType
     * May be deprecated in future
     *
     * @param projectType
     * @throws ProjectTypeConstraintException
     */
    public void registerProjectType(ProjectTypeDef projectType) throws ProjectTypeConstraintException {
        if (isNameValid(projectType) && isParentValid(projectType, validatedData)) {
            validatedData.put(projectType.getId(), projectType);
            init(projectType);
        }
    }

    /**
     * @param id
     * @return project type by id
     */
    public ProjectTypeDef getProjectType(String id) throws NotFoundException {
        final ProjectTypeDef pt = projectTypes.get(id);
        if (pt == null) {
            throw new NotFoundException("Project Type not found: " + id);
        }
        return pt;
    }

    /**
     * @return all project types
     */
    public Collection<ProjectTypeDef> getProjectTypes() {
        return projectTypes.values();
    }

    /**
     * @param comparator
     * @return all project types sorted with specified comparator
     * @see ProjectTypeRegistry.ChildToParentComparator
     */
    public List<ProjectTypeDef> getProjectTypes(Comparator<ProjectTypeDef> comparator) {
        List<ProjectTypeDef> list = projectTypes.values().stream().collect(Collectors.toList());
        Collections.sort(list, comparator);
        return list;
    }

    /**
     * project type comparator which sorts collection of project types in child-to-parent order
     */
    public static class ChildToParentComparator implements Comparator<ProjectTypeDef> {
        @Override
        public int compare(ProjectTypeDef o1, ProjectTypeDef o2) {
            if (o1.isTypeOf(o2.getId())) {
                return -1;
            }
            if (o2.isTypeOf(o1.getId())) {
                return 1;
            }
            return 0;
        }
    }

    /**
     * Validates incoming set of Project Type definitions
     * and forms preliminary collection of validated data to be initialized
     *
     * @param types
     */
    protected final void validate(Collection<? extends ProjectTypeDef> types) {
        Map<String, ProjectTypeDef> pass1 = new HashMap<>();

        if (!types.contains(BASE_TYPE)) {
            pass1.put(BASE_TYPE.getId(), BASE_TYPE);
        }

        types.stream()
             .filter(this::isNameValid)
             .forEach(type -> pass1.put(type.getId(), type));

        // look for parents
        pass1.values()
             .stream()
             .filter(type -> isParentValid(type, pass1))
             .forEach(type -> validatedData.put(type.getId(), type));
    }

    /**
     * Checks if incoming Project Type definition has valid ID (Pattern.compile("[^a-zA-Z0-9-_.]")
     * and display name (should not be null or empty)
     *
     * @param type
     * @return true if valid
     */
    private boolean isNameValid(ProjectTypeDef type) {

        boolean valid = true;

        if (type.getId() == null || type.getId().isEmpty() || NAME_PATTERN.matcher(type.getId()).find()) {
            LOG.error("Could not register Project Type ID is null or invalid (only Alphanumeric, dash, point and underscore allowed): "
                      + type.getClass().getName());
            valid = false;
        }

        if (type.getDisplayName() == null || type.getDisplayName().isEmpty()) {
            LOG.error("Could not register Project Type with null or empty display name: " + type.getId());
            valid = false;
        }

        for (Attribute attr : type.getAttributes()) {
            // ID spelling (no spaces, only alphanumeric)
            if (NAME_PATTERN.matcher(attr.getName()).find()) {
                LOG.error("Could not register Project Type with invalid attribute Name (only Alphanumeric, dash and underscore allowed): " +
                          attr.getClass().getName() + " ID: '" + attr.getId() + "'");
                valid = false;
            }
        }

        return valid;
    }

    /**
     * Validates if Project Tye definition has  parent names which are already registered
     *
     * @param type
     * @param pass1
     * @return
     */
    private boolean isParentValid(ProjectTypeDef type, Map<String, ProjectTypeDef> pass1) {

        boolean contains = true;

        for (String parent : type.getParents()) {
            if (!pass1.keySet().contains(parent)) {
                LOG.error("Could not register Project Type: " + type.getId() + " : Unregistered parent Type: " + parent);
                contains = false;
            }
        }

        // add Base Type as a parent if not pointed
        if (type.getParents() != null && type.getParents().isEmpty() && !type.getId().equals(BASE_TYPE.getId())) {
            type.addParent(BASE_TYPE.getId());
        }

        return contains;
    }

    /**
     * validates and initializes concrete project type
     *
     * @param type
     * @throws ProjectTypeConstraintException
     */
    protected final void init(ProjectTypeDef type) throws ProjectTypeConstraintException {
        initRecursively(type, type.getId());

        if (!type.factoriesToOverride.isEmpty()) {
            overrideFactories(type);
        }

        this.projectTypes.put(type.getId(), type);

        LOG.debug("Project Type registered: " + type.getId());
    }

    /**
     * initializes all the attributes defined in myType and its ancestors recursively
     *
     * @param myType
     * @param typeId
     *         temporary type for recursive (started with initial type)
     * @throws ProjectTypeConstraintException
     */
    private final void initRecursively(ProjectTypeDef myType, String typeId) throws ProjectTypeConstraintException {
        ProjectTypeDef type = validatedData.get(typeId);

        for (String superTypeId : type.getParents()) {
            myType.addAncestor(superTypeId);

            ProjectTypeDef supertype = validatedData.get(superTypeId);
            for (Attribute attr : supertype.getAttributes()) {
                // check attribute names
                for (Attribute attr2 : myType.getAttributes()) {
                    if (attr.getName().equals(attr2.getName()) && !attr.getProjectType().equals(attr2.getProjectType())) {
                        throw new ProjectTypeConstraintException("Attribute name conflict. Project type " +
                                                                 myType.getId() + " could not be registered as attribute declaration " +
                                                                 attr.getName() +
                                                                 " is duplicated in its ancestor(s).");
                    }
                }
                myType.addAttributeDefinition(attr);
            }
            initRecursively(myType, superTypeId);
        }
    }

    private final void overrideFactories(ProjectTypeDef myType) throws ProjectTypeConstraintException {
        for (Map.Entry<String, ValueProviderFactory> entry : myType.factoriesToOverride.entrySet()) {
            Attribute old = myType.getAttribute(entry.getKey());
            if (old == null || !old.isVariable()) {
                throw new ProjectTypeConstraintException("Can not override Value Provider Factory. Variable not defined: "
                                                         + myType.getId() + ":" + entry.getKey());
            }

            myType.attributes.put(old.getName(), new Variable(old.getId(),
                                                              old.getName(),
                                                              old.getDescription(),
                                                              old.isRequired(),
                                                              entry.getValue()));

        }

    }
}
